<?php

/**
 * @package    Europa
 * @subpackage Controller
 */



/*
 * Autoloading
 * 
 * Autoloading significantly increases performace, therefore is enabled
 * automatically, making it a requirement for Europa to run.
 * 
 * We first check to see if the Standard PHP Library exists. If it does
 * then it is used for autoloading. If not, then we fallback to __autoload.
 */

// require the loader if it isn't defined yet
if (!class_exists('Europa_Loader')) {
	require 'Loader.php';
}

// add Europa's directory to the list of load paths
Europa_Loader::addLoadPath(dirname(__FILE__) . '/../');

// register the autoload function
if (function_exists('spl_autoload_register')) {
	spl_autoload_register(array('Europa_Loader', 'loadClass'));
} else {
	function __autoload($className)
	{
		Europa_Loader::loadClass($className);
	}
}



/*
 * Sets the default exception handler so that a Europa_Exception can be thrown 
 * without being wrapped in a try/catch block.
 */
set_exception_handler(array('Europa_Exception', 'handle'));



/**
 * The heart of EuropaPHP. This is where it all starts and ends.
 */
class Europa_Controller extends Europa_Abstract
{
	const
		/**
		 * Contains the event name used to trigger the event that gets called
		 * before dispatching.
		 */
		EVENT_PRE_DISPATCH = 'Europa_Controller.preDispatch',
		
		/**
		 * Contains the event name used to trigger the event after dispatching
		 * is complete.
		 */
		EVENT_POST_DISPATCH = 'Europa_Controller.postDispatch',
		
		/**
		 * The exception/error code that identifies and exception with a controller
		 * not being found.
		 */
		EXCEPTION_CONTROLLER_NOT_FOUND = 1,
		
		/**
		 * The exception/error code that identifies and exception with a action
		 * not being found.
		 */
		EXCEPTION_ACTION_NOT_FOUND = 2,
		
		/**
		 * Fired when a required parameter inside an action is not defined in the request.
		 */
		EXCEPTION_REQUIRED_PARAMETER_NOT_DEFINED = 3;
	
	public
		/**
		 * The installation directory where the current application is installed. This is will
		 * be the absolute path to the directory that the file which called Europa_Controller is in.
		 */
		$rootDir = null,
		
		/**
		 * Contains the Europa root uri. This is the base installation uri of Europa. If your
		 * application is installed at http://example.com/my/uri/to/my-app/, this would
		 * contain my/uri/to/my-app.
		 */
		$rootUri = null,
		
		/**
		 * Contains the Europa request uri. If Europa is installed at http://example.com/eu/
		 * and the URI was http://example.com/eu/my/request/uri/, this would contain my/request/uri.
		 */
		$requestUri = null,
		
		/**
		 * After dispatching, this will contain the route that was used to reach the 
		 * current page. This can be set before dispatching to force a route to be
		 * taken.
		 */
		$route = null,
		
		/**
		 * This will contain the controller that was found using the matched route.
		 * This can be set before dispatching to override the default controller
		 * name.
		 */
		$controller = 'index',
		
		/**
		 * Similar to Europa_Controller::$controller, this can also be set 
		 * pre-dispatch to set the default action to route to. This will contain the action 
		 * that was found using the matched route.
		 */
		$action = 'index',
		
		/**
		 * Similar to Europa_Controller::$layout, Europa_Controller::view 
		 * contains the view object that will be rendered. To disable the view, you can 
		 * set this to null.
		 */
		$view = null;
	
	private
		/**
		 * This is where routes are set and it should contain all routes that are to 
		 * be matched against. The array keys will be regular expressions and the values
		 * will be the replacements for the regular expression as if you were using
		 * preg_replace($routeKey, $routeVal).
		 */
		$_routes = array();
	
	static public
		/**
		 * Overrides the Europa_Abstract configuration and gives a default configuration to Europa_Controller.
		 */
		$defaultConfig = array(
				'controllerPath'   => './app/controllers',
				'controllerPrefix' => '',
				'controllerSuffix' => 'Controller',
				'actionPrefix'     => '',
				'actionSuffix'     => 'Action'
			);
		
	static private
		/**
		 * Contains the instances of all controllers that are currently dispatching in chronological order.
		 */
		$_dispatchStack = null;
	
	
	
	/**
	 * Constructs the parent and sets good defaults, meaning less configuration.
	 * 
	 * @param Array[Optional] $config An array of configuration variables.
	 * 
	 * @return Europa_Controller
	 */
	public function __construct($config = null)
	{
		parent::__construct($config, self::$defaultConfig);
		
		// set the rootUri, this can be overridden
		$this->rootUri = trim(dirname($_SERVER['PHP_SELF']), '/');
		
		// sniff the root directory on iis
		if (!isset($_SERVER['DOCUMENT_ROOT'])) {
			$localPath = isset($_SERVER['PATH_INFO'])
				? $_SERVER['PATH_INFO']
				: $_SERVER['SCRIPT_NAME'];
			
			$localPath = str_replace('/', DIRECTORY_SEPARATOR, $localPath);
			
			$absolutePath = isset($_SERVER['PATH_TRANSLATED'])
				? $_SERVER['PATH_TRANSLATED']
				: $_SERVER['SCRIPT_FILENAME'];
			
			$_SERVER['DOCUMENT_ROOT'] = substr($absolutePath, 0, strpos($absolutePath, $localPath));
		}
		
		// set the root directory
		$this->rootDir = trim($_SERVER['DOCUMENT_ROOT'] . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $this->rootUri));
		
		// remove the root uri from the request uri to get the relative request uri for the framework
		$uri = isset($_SERVER['HTTP_X_REWRITE_URL']) ? $_SERVER['HTTP_X_REWRITE_URL'] : $_SERVER['REQUEST_URI'];
		$uri = trim($uri, '/');
		$uri = substr($uri, strlen($this->rootUri));
		$uri = trim($uri, '/');
		$uri = explode('?', $uri);
		$uri = $uri[0];
		
		// the page we are on
		$this->requestUri = $uri;
		
		// set a default view
		$this->view = new Europa_View_Layout();
		
		// set the default view to render
		$this->view->render('index/index');
	}
	
	/**
	 * Fires dispatching 
	 * 
	 * @param string[optional] $route A custom route to pass to the dispatch call. This will override any of the set routes.
	 * 
	 * @return Europa_Controller
	 */
	public function dispatch()
	{
		// fire the pre dispatch event before anything else
		Europa_Event::trigger(self::EVENT_PRE_DISPATCH, array('Europa_Controller' => $this));
		
		// we add this dispatch instance to the stack
		self::$_dispatchStack[] = $this;
		
		// add a default route if it doesn't exist
		if (!isset($this->_routes['default'])) {
			$this->addRoute('default', new Europa_Route('/?([^/]+)/?', array('controller', 'action'), ':controller/:action'));
		}
		
		// process routes
		foreach ($this->_routes as $name => $route) {
			if ($route->process()->hasMatch()) {
				$this->route = $route;
				
				$controller = $this->route->getController();
				$action     = $this->route->getAction();
			}
		}
		
		// set which view to render
		$this->view->render($controller . '/' . $action);
		
		// set the controller and action names
		$controllerName = new Europa_String($this->getConfig('controllerPrefix') . $controller . $this->getConfig('controllerSuffix'));
		$actionName     = new Europa_String($this->getConfig('actionPrefix')     . $action     . $this->getConfig('actionSuffix'));
		$controllerName = (string) $controllerName->camelCase(true);
		$actionName     = (string) $actionName->camelCase(false);
		
		// load the controller
		if (!Europa_Loader::loadClass($controllerName, $this->getConfig('controllerPath'))) {
			throw new Europa_Exception('Could not load controller <strong>' . $controllerName . '</strong> from <strong>' . $this->getConfig('controllerPath') . '</strong>.');
		}
		
		// reverse engineer the controller
		$controllerReflection = new ReflectionClass($controllerName);
		
		// instantiate the controller
		$controllerInstance = $controllerReflection->newInstanceArgs();
			
		// so we can refer to the view in the controller
		$controllerInstance->view = $this->view;
		
		// call the init method, like __construct, but set properties are now available
		if ($controllerReflection->hasMethod('init')) {
			$controllerInstance->init();
		}
		
		// generate values for the parameters in the action
		// named parameters will be set to their corresponding names as defined in the action
		// non-named parameters will be set according to their index
		// required parameters must be set, or an exception will be thrown
		if ($controllerReflection->hasMethod($actionName) || $controllerReflection->hasMethod('__call')) {
			$actionReflection = $controllerReflection->getMethod($actionName);
			$actionParams     = array();
			$routeParams      = $this->route->getParams();
			
			// automatically define the parameters that will be passed to the action
			foreach ($actionReflection->getParameters() as $paramIndex => $param) {
				$pos  = $param->getPosition();
				$name = $param->getName();
				
				// cascade from named parameters to index offsets then to default values
				// if a required parameter isn't defined, an exception is thrown
				if (array_key_exists($name, $routeParams)) {
					$actionParams[$pos] = $routeParams[$name];
				} elseif (array_key_exists($pos, $routeParams)) {
					$actionParams[$pos] = $routeParams[$pos];
				} elseif ($param->isOptional()) {
					$actionParams[$pos] = $param->getDefaultValue();
				} else {
					throw new Europa_Exception('Required request parameter <strong>$' . $name . '</strong> for <strong>' . $controllerName . '->' . $actionName . '()</strong> is not set.', self::EXCEPTION_REQUIRED_PARAMETER_NOT_DEFINED);
				}
			}
			
			$actionReflection->invokeArgs($controllerInstance, $actionParams);
		} else {
			Europa_Exception::trigger('Action <strong>' . $actionName . '</strong> does not exist in <strong>' . $controllerName . '</strong> and it was not trapped in <strong>__call</strong>.', self::EXCEPTION_ACTION_NOT_FOUND);
		}
		
		// pre rendering hook
		if ($controllerReflection->hasMethod('preRender')) {
			$controllerInstance->preRender();
		}
		
		// no rendering will occur if view is not set
		if ($controllerInstance->view instanceof Europa_View) {
			echo $controllerInstance->view;
			
			// post rendering hook
			if ($controllerReflection->hasMethod('postRender')) {
				$controllerInstance->postRender();
			}
			
			// unset it, calling the __destruct method
			unset($controller);
		}
		
		// now we remove it from the dispatch stack
		unset(self::$_dispatchStack[count(self::$_dispatchStack) - 1]);
		
		// fire the post dispatch event after everything is done
		Europa_Event::trigger(self::EVENT_POST_DISPATCH, array('Europa_Controller' => $this));
	}
	
	public function addRoute($name, $route)
	{
		$this->_routes[$name] = $route;
	}
	
	
	
	/**
	 * Returns the Europa_Controller instance that is currently dispatching.
	 * 
	 * @return mixed
	 */
	static public function getActiveInstance()
	{
		$len = count(self::$_dispatchStack);
		
		// if there are dispatched instances, then return the latest one
		if ($len) {
			return self::$_dispatchStack[$len - 1];
		}
		
		return null;
	}
	
	/**
	 * Returns all Europa_Controller instances that are dispatching, in chronological order, as an Array.
	 * 
	 * @return array
	 */
	static public function getDispatchStack()
	{
		return self::$_dispatchStack;
	}
	
	
	
	/**
	 * Takes a value and type casts it. Strings such as 'true' or 'false' will be converted to
	 * a boolean value. Numeric strings will be converted to integers or floats and empty
	 * strings are converted to NULL values.
	 *       
	 * @param mixed $val The value to cast and return.
	 * 
	 * @return mixed
	 */
	static private function _typeCast($val)
	{
		$val = urldecode($val);
		
		if ($val == 'true') {
			return true;
		}
		
		if ($val == 'false') {
			return false;
		}
		
		if (is_string($val) && is_numeric($val)) {
			return strpos($val, '.') === false
				? (int) $val
				: (float) $val;
		}
		
		if ($val == '') {
			return null;
		}
		
		return $val;
	}
}